JavaScript の Module System の歴史
#JavaScript #TypeScript #歴史
モチベーション
近頃業務で TypeScript を書くようになったのだが、モジュールの扱いがいまいち理解できていなかった。公式サイトではモジュールを扱うには import / export が基本とされていたが、他の人のコードレビューをしようとしたら require でインポートされていたりして、なぜそれが動くのかとかどうすれば良いかとかもわからなかった (しかもお互い TypeScript は初学者だった)。
JavaScript はモジュール周りで色々あったということだけは小耳に挟んでいたけど、詳しい事情は把握できていなかったのでこの機会にまとめてみる。
JavaScriptのモジュールシステムの歴史と現状
下記を全部読めばだいたいわかる。
Brief history of JavaScript Modules
JavaScript の経緯
JavaScript は、そもそもクライアントサイド、Web ブラウザー上で動作する言語として生まれた。のだと思う。ここら辺は詳しく調べていないので嘘を言っているかもしれない。とにかく、初期の JavaScript は Web ブラウザー上で実行されるもので、HTML 内にそのコードが記載されていた。その頃からどうやって人々はコードを分割、モジュール化していたのかを追っていく。
インラインスクリプト
code:html
<!DOCTYPE html>
<html>
<body>
<span id="test"></span>
<script type="text/javascript">
function add(a, b) {
return a + b;
}
document.getElementById("test").innerHTML = add(2, 3);
</script>
</body>
</html>
HTML 上で script タグ内にインラインで埋め込む方式で、JS は記述できる。インラインスクリプトはかなり簡単にはかけるが、以下のデメリットがある。
コードの再利用ができない
他のページでも JavaScript のコードが使いたい場合にコピペするしかない
関数間の依存関係の解決ができない
main 関数内で利用したい別の関数がある場合は、main の前にそれらを定義する必要がある
グローバル汚染
全ての変数及び関数がグローバルなスコープに存在する
script タグによる JS ファイルの読み込み
code:html
<!DOCTYPE html>
<html>
<body>
<span id="test"></span>
<script type="text/javascript" src="./add.js"></script>
<script type="text/javascript" src="./main.js"></script>
</body>
</html>
script タグによって、JS コードを別ファイルに切り出し、それを読み込むことができる。これによりコードの再利用は可能になったが、依然として 依存関係解決の問題 や グローバル汚染 の問題がある。
IIFE モジュールパターン
IIFE とは Immediately-invoked function expression の略で、日本語だと 即時実行関数式 と略されていたりする。IIFE のフォーマットは (function(){ /* ... */ })(); のような形で、その内部で定義された変数や関数はその内部のローカルスコープでのみ扱える。これにより、グローバル汚染を防ぐことができる。
逆に、内部から外部に関数や変数を expose したい場合は、以下のように、グローバルスコープで定義されたオブジェクトに内部からアクセスし、格納する。
code:javascript
// グローバルスコープ
var myApp = {};
// 別ファイルに切り出された IIFE 形式のモジュール
(function(){
myApp.add = function(a, b) {
return a + b;
}
})();
// グローバルスコープの変数名を短いエイリアスで扱いたい場合
// 引数を介してモジュール内部に渡すようなテクニックもある
(function(app){
app.add = function(a, b) {
return a + b;
}
})(myApp);
// グローバルスコープ
var result = myApp.add(1,2);
console.log(result); // 3
JavaScript のライブラリにはこの形式をとっているものが多い。例えば、jQuery なんかは $ というグローバルスコープのオブジェクトに対し、この形式で関数や変数を生やすことで、名前空間の集約を行なっている。
しかし、この形式でも今だに 依存関係解決の問題 は残る。また、グローバル汚染もだいぶ解消はされたものの、1 つのグローバル変数が結局必要 になってしまう。
CommonJS
CommnoJS については CommonJS に詳しく書く。一言で言うと Server Side JavaScript の仕様のこと。Server Side の仕様であるため、.js ファイルに分割して <script> タグで読み込む、といった前述の手法は利用できない。そのため、新たなモジュールのための API 仕様が必要となった。
code:javascript
// 外部に公開 (export) する側
module.exports = function add(a, b){
return a+b;
}
// 公開されたものを利用 (import) する側
var add = require(‘./add’);
Node.js で見慣れた形式だが、Node.js の require 関数は同期関数、CommonJS のは非同期関数、という 話を見かけた が、詳しい調査はできてない。
これだとグローバル汚染や依存関係解決の問題は発生しなくなったが、今度は以下のような特徴が問題視されはじめた。
同期呼び出し
例えば、コード中に var add=require('add') のような記述があった場合、その行は モジュール add がロードされるまで待ち続けてしまう
つまり、例えばブラウザ上でページを開いたときに、全てのモジュールがロードされるまで処理が停止してしまう
したがって、ブラウザ側ではあまりよいソリューションではないとされている
AMD (Asynchronous Module Definition)
CommonJS では、サーバサイドのモジュールのシンタックスをクライアントサイドでも利用できるようにするために、いくつかのモジュールフォーマットを Module/Transfer として公開した。この中の一つである Module/Transfe/C がのちに AMD と呼ばれる仕様である。
code:javascript
// define は、
// param1: 依存するモジュールのリスト
// param2: モジュールを利用するコールバック関数
define(‘add’, ‘reduce’, function(add, reduce){
// ここで return される値が外部に export される
return function(){...};
});
そして、この仕様を利用できるようにするための実装の一つが RequireJS である。
RequireJS はその名前からして CommonJS の require シンタックスを利用可能にするためのもののように思えるが、そうではなく、AMD スタイルでモジュールをインポートできるようにするための モジュールローダー である。
code:html
<!DOCTYPE html>
<html>
<body>
...
<!-- エントリーポイントを指定して RequireJS をロードする -->
<!-- .js は省略可能 -->
<script data-main="main" src="require.js"></script>
</body>
</html>
code:javascript
// main.js
// これがロードされた後、RequireJS はその依存先の other モジュールをロードしに行く
define('other', function(other){
...
})
これで、非同期にモジュールをロードできるようになったし、グローバル汚染もなく、依存関係の問題、すなわち関数のロード順についても気にしなくて良くなった。しかし、まだ以下のような問題があった。
記述が冗長
define でラップする必要がある
引数の順番を気にする必要がある
コールバック関数では、それ以前の引数の順番通りにモジュールを引数に取る必要がある
パフォーマンスに影響がある
HTTP1.1 では、たくさんの細かいファイルをロードするのはパフォーマンスに影響する
Browserify
Browserify は、CommonJS のモジュールの記法である export, require をクライアントサイドで利用できるようにするための モジュールバンドラー として登場した。Browserify は、JavaScript コードの依存ツリーを解析し、JS を 1 つのファイルにバンドルする。
UMD
ここまでで、モジュールの記法としては以下が登場した。
グローバルオブジェクト
CommonJS
AMD
どれを使うかはオプションとなっている。ユーザは自身が利用するモジュールがどれで記述されているのか判断し、利用できる必要がある、という問題がある。
UMD とは Universal Module Definition の略称であり、現在の環境がサポートしているモジュールスタイルを識別する。
code:javascript
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define('add', 'reduce', factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('add'), require('reduce'));
} else {
// Browser globals (root is window)
root.sum = factory(root.add, root.reduce);
}
}(this, function (add, reduce) {
// private methods
// exposed public methods
return function(arr) {
return reduce(arr, add);
}
}));
ES6 moudle syntax
グローバルオブジェクト、CommonJS、AMD、UMD と、ここまでで JavaScript のモジュールの記法が様々登場した。これは、JavaScript の言語仕様にモジュールに関する仕様がなかったために生じた問題である。
ES6 では、よやくモジュールの仕様が言語仕様に含まれるようになった。ES6 の記法では、export および import が記法としては用いられる。
code:javascript
// main.js
import sum from "./sum";
var values = 1, 2, 4, 5, 6, 7, 8, 9 ;
var answer = sum(values);
document.getElementById("answer").innerHTML = answer;
基本的には新規プロジェクトでは ES6 の記法を用いるのが望ましいが、ブラウザのサポートが追随できていないという問題がある。
Webpack
webpack は モジュールバンドラー である。Webpack の Browserify との違いは、Webpack は CommonJS、AMD、ES6 モジュールを扱える点である。そのほかにも、様々な機能がある。
Rollup
多すぎる
SystemJS
???
JSPM
......
TypeScript
TypeScript のモジュールについては下記に記述されている。
https://www.typescriptlang.org/docs/handbook/modules.html
TypeScript 1.5 より前は、内部モジュール、外部モジュールという概念があったようだが、ECMAScript 2015 の概念に合わせて呼び名が各々 namespaces、modules に変更となった、という経緯があるようだ。
ECMAScript 2015 から、JavaScript はモジュールの概念を導入した。TypeScript もこの概念を共有する。
モジュール は、グローバルではない各々のスコープ内で実行される
明示的に export されていない値や関数は、モジュール外から参照できない
export で外だしして、import で利用する
Module loader
各々の実行前に、依存関係にある全てのモジュールをランタイムにロードする
Node.js では CommonJS、Web アプリケーションでは require.js が有名
TypeScript では
トップレベルに import もしくは export をもったファイルは全てモジュールと考えられる
逆に、トップレベルに import もしくは export がないファイルは、グローバルなスコープで利用可能なスクリプトとして扱われる
指定されたモジュールターゲットによって、コンパイラが生成するコードは異なる。コンパイラが生成できるコードは以下である。
CommonJS (Node.js)
AMD (require.js)
UMD
SystemJS
ES6 (ECMAScript 2015 native module)